用设计语言解读「一起展」App 技术理念与产品哲学的映射

本文基于完整开发过程整理,力求还原每一处产品设计与技术实现的细节,供记忆与理念深挖。
<!–


目录


–>

零、技术理念与产品哲学的映射

技术实现 产品/设计理念
「一分钟创建展览」的快速布展 降低策展门槛,让艺术表达平民化
展厅透视+画框材质渲染 线上展览不是简单图片排列,而是空间与审美的还原
视频/音频/图文的多媒体混合 不同内容形态适配不同表达方式
笔记两级浏览(预览→全文) 尊重用户注意力,先吸引再深入
背景音乐与视频的音频焦点管理 细节处的沉浸感打磨
邀约办展机制 平台对优质内容的筛选与背书
实名认证前置 社区治理与内容可信度的基础建设
thumbnailLevel 分级加载 性能与体验的平衡,不因省流量牺牲画质

一、产品定位与架构概览

「一起展」是一款围绕展览、笔记、生活秀构建的内容社区与创作工具型 App。用户既可以浏览他人发布的展览与图文笔记,也可以通过「一分钟创建展览」快速搭建自己的线上展厅,形成从内容消费到内容生产的完整闭环。

核心架构特征:

  • 模块化 Monorepo:采用 apps/host_app 壳工程 + packages/features/* 业务包拆分,严格遵循单向依赖(apps → features → common → core),业务包之间绝对隔离。
  • 状态管理:全面使用 flutter_riverpodStateNotifierProviderAsyncNotifierProviderStateProvider),已完成从 Provider 的迁移。
  • 路由体系:基于 go_router 的统一命名路由,所有跨模块跳转通过 core_router 实现,禁止直接 import 目标页面类文件。
  • 媒体处理:视频使用 video_player,音频使用 just_audio,图片使用 CachedNetworkImage,本地媒体选取使用 image_picker
  • 数据埋点:集成 Matomo 分析,页面进入、按钮点击、视频播放、发布漏斗等关键行为均带 matomoTitle 上报。

二、启动与认证体系

2.1 启动页(Splash)

  • 纯品牌展示页,作为应用入口;完成后根据登录态决定跳转登录页或首页。

2.2 登录页(LoginPage)

设计亮点:

  • 沉浸式登录氛围:页面顶部使用全宽品牌背景图(assets/icons/login/login_bg.png),下方承载登录表单,视觉层次分明。
  • 双通道登录:支持「手机号 + 短信验证码」与「微信授权登录」两种方式。
  • 手机号输入细节
    • 左侧固定展示 +86 区号选择器;
    • 输入框底部边框随焦点状态动态变色(聚焦高亮蓝色,失焦灰色);
    • 支持一键清空输入(右侧 clear 图标);
    • 输入限制:仅数字、最多 11 位。
  • 验证码输入细节
    • 与手机号输入框共享一致的设计语言;
    • 右侧「获取验证码」按钮带倒计时状态(countdown),发送中展示 CircularProgressIndicator
    • 倒计时期间按钮置灰并显示剩余秒数。
  • 协议同意机制:登录按钮下方放置勾选框,必须勾选《用户协议》与《隐私政策》才能触发登录;文案可点击跳转对应协议页。未勾选时点击登录会弹出 SnackBar 提示。
  • 登录按钮:紫蓝渐变背景(#6A11CB → #2575FC),宽度铺满,圆角 8px;仅当手机号 11 位且验证码 6 位时才可点击。
  • 其他登录方式区:独立区块展示「其他登录方式」文案,居中放置微信图标按钮,点击唤起微信授权。
  • 登录后路由:登录成功优先 pop 返回上一页;若无上一页则 pushReplacementNamed 到首页。
  • 异常处理:捕获 DioException,对 HandshakeException 给出明确的「网络证书校验失败」提示,便于调试。

2.3 绑定手机号(BindPhonePage)

  • 微信登录后若后端返回 NeedBindPhoneException,强制引导至绑定手机号页,完成后方可进入主流程。

2.4 协议页(AgreementPage)

  • 支持《用户协议》与《隐私政策》两种类型,通过 AgreementType 枚举区分,纯文本展示。

三、首页信息流(HomePage)

首页是整个 App 的核心流量入口,采用底部导航 + 顶部 Tab 双层结构

3.1 底部导航栏(4 大板块)

Tab 说明
首页 信息流主阵地,含 6 个二级频道
办展 展览分类浏览与快速布展入口
邀约 活动/邀请函相关(预留入口)
我的 个人中心与内容管理

设计细节:

  • 底部导航栏图标与文案配合,选中态有明确视觉反馈。
  • 状态栏图标颜色自适应:当位于「视频」频道时,状态栏使用浅色图标(Brightness.light),其余频道使用深色图标(Brightness.dark),避免从视频/看展页返回后状态栏颜色残留导致白底不可见。
  • 页面根节点包裹 AnnotatedRegion<SystemUiOverlayStyle>,确保系统 UI 风格与当前内容一致。

3.2 顶部频道 Tab(6 个频道)

推荐、视频、热展、直通车、关注、上海

3.2.1 推荐频道

  • 典型的瀑布流笔记列表,卡片高度不一,形成视觉节奏。
  • 笔记卡片包含:封面图/视频、标题、作者头像与昵称、点赞数。
  • 支持无限滚动加载,带骨架屏占位(AppSkeletonHallGrid)。

3.2.2 视频频道(HomeVideoTabPage)

核心体验设计:

  • 垂直 PageView 全屏视频流:模拟抖音/快手的沉浸式短视频体验,每个视频占满一屏。
  • 自定义滑动物理效果_ShortDragPageScrollPhysics 允许更短的拖拽距离触发翻页,提升操作跟手性。
  • 视频预加载策略:开始播放约 5 秒后预热下一条视频,优先保证滑动切换时减少黑屏与卡顿。
  • 交互手势
    • 单击:播放/暂停切换;
    • 双击:触发点赞动画(红心浮起);
    • 长按:触发倍速播放(2x/3x),松手恢复常速。
  • 错误降级:视频加载失败时自动回退到封面图展示,避免黑屏。
  • 埋点:自动上报视频观看时长、播放错误率等事件。

3.2.3 热展频道

  • 展示热门展览卡片,点击进入展览详情页(ExhibitViewingPageTemplate1)。
  • 展览封面解析有专门策略:优先使用 visitFileId 作为海报;若展位首项为图片则海报+首张展位图轮播;若首项为视频则仅展示海报。

3.2.4 直通车 / 关注 / 上海

  • 直通车:平台精选或商业推广内容入口。
  • 关注:展示已关注用户的动态。
  • 上海:基于地域(上海)的内容聚合,体现本地化运营思路。

3.4 搜索入口

  • 首页顶部导航栏右侧带有搜索图标,点击触发 Matomo 埋点(category: 'search', action: 'entry_click', name: 'home_headbar')。
  • 搜索功能当前为入口占位,后续可扩展为全局内容搜索(笔记/展览/用户)。

3.5 首页生命周期与数据一致性

  • didPopNext 自动刷新:首页监听 RouteObserver,当用户从其他页面(如笔记详情、展览页、编辑页)返回时,自动触发当前 Tab 的数据刷新,确保点赞、关注、删除等操作的结果即时同步到列表。
  • 首次加载优化:Tab 切换时首次 invalidate 对应 Provider 触发加载;已加载过的 Tab 不重复请求,除非用户手动下拉刷新。
  • 状态栏动态适配:首页根节点包裹 AnnotatedRegion<SystemUiOverlayStyle>,视频 Tab 使用 SystemUiOverlayStyle.light,其余 Tab 使用 SystemUiOverlayStyle.dark,避免从视频/看展页返回后状态栏图标颜色残留导致白底不可见。

3.3 笔记创建(HomeCreateNotePage)

设计亮点:

  • 媒体选择互斥:图片与视频不能同时存在,选中一类后另一类入口自动隐藏。
  • 富文本编辑:支持输入标题与正文,正文支持话题提取与插入(#话题# 形式)。
  • 草稿机制:支持保存草稿、加载草稿继续编辑,草稿数量在个人主页展示。
  • 隐私分级:「公开可见」改造为一级隐私选择 + 二级联系人选择抽屉:
    • 公开 / 互关可见 / 私密
    • 部分可见 / 不让谁看:支持搜索联系人、多选、显示已选人数与确认回填。
  • 位置标签:支持添加地理位置。
  • 发布流程:先上传媒体文件,拿到 fileId 后再调用发布接口,失败时保留草稿。

四、办展与展览系统(Exhibit)

这是 App 最具差异化的功能模块,将「线上策展」轻量化,让普通用户也能「一分钟创建展览」。

4.1 办展分类页(ExhibitTabPage)

视觉设计:

  • 顶部有大幅 Banner 背景图,文案「一分钟创建展览!」,右侧 CTA「立即办展」。
  • Banner 与下方内容区有重叠效果(exhibitContentTopOverlap),内容区以圆角(exhibitContentTopRadius)裁切浮起,形成卡片层叠感。
  • 分类导航:横向滚动的一级分类 Tab,带底部指示器(圆角小条);下方有二级/三级分类 Chip 筛选。
  • 邀约角标:部分分类带有「邀约」角标,金橙渐变背景(#FFD09D → #DE8220),圆角不对称设计(左上/右上/右下圆角 6,左下圆角 2),精致且有辨识度。
  • 展馆瀑布流:选中分类后展示对应模板/展馆卡片,支持下拉刷新与上拉加载更多;无数据时展示「暂无展馆」空状态。
  • 列表底部:到达末尾时展示趣味文案「恭喜你发现了世界的尽头!🌈」配分割线,缓解无更多内容的失落感。
  • 实名提醒弹窗:双击 Banner CTA 区域触发「实名提醒」弹窗,引导用户完成实名认证解锁办展功能。

4.2 办展封面页(ExhibitCoverPageTemplate1)

  • 展示单个模板/展馆的封面大图(全景图比例 393:262)、标题、关键词标签(橙色背景 #FF8C00 半透明)、推荐描述。
  • 底部渐变遮罩 +「开始布展」大按钮,深蓝渐变(#11317C → #1B46AA → #224598)。
  • 邀约拦截:若当前模板所属分类标记为 requiresInvitation=true,点击「开始布展」不进入编辑,而是重置底部导航到办展 Tab 并回到首页。

4.3 快速布展(FastExhibitPage)

核心交互设计:

  • 多展厅管理:支持最多 6 个展厅,底部横向缩略图列表展示各厅;默认 1 号厅选中,右侧「新增展厅」按钮带加号叠加;仅允许删除非一号厅。
  • 展位作品管理
    • 空展位:直接点击唤起媒体选择(图片);
    • 已填充展位:点击弹出蒙层,可选择「更换作品」或「编辑查看」;
    • 长按/批量选择模式:支持多选后批量删除。
  • 展厅切换:底部缩略图点击切换,主舞台实时加载对应展厅图片;空位不再回填默认图,保持真实创作状态。
  • 底部操作栏:居中分布「信息」「音乐」「清空」按钮,清晰易触。
  • 退出确认:返回时若存在未保存内容,弹窗询问「保存为草稿」或「放弃离开」。
  • 状态管理:使用 FastExhibitDraftControllerStateNotifier)管理 hallDrafts,每个展厅独立维护 entryLocalPathboothLocalPaths

4.4 发布页(FastExhibitPublishPage)

  • 全景预览:上方展示当前选中展厅的透视效果,展位按模板 boothList.length 动态渲染(不再固定 5 个图位),支持画框材质、阴影、厚度。
  • 展厅缩略图切换:底部横向缩略图可切换预览不同展厅。
  • 表单信息:填写展览名称、推荐描述、选择定位、隐私设置。
  • 提交逻辑
    • 先调用 saveOrUpdate 创建展览(esQuantity = boothList.length * hallCount,状态草稿=1/发布=2);
    • 拿到 id 后二次调用同接口编辑提交 id/templateName/recommendDesc/fileId/isType/scopeUserIds/boothList
  • 发布后路由:发布成功 → 首页「我的」Tab;保存草稿 → 草稿箱「展览」Tab。
  • 动态展位布局exhibit_wall_artworks_perspective.dart 支持动态展位布局与可选 frameDataList/frameMaterialUrls,可渲染边框、阴影、厚度与材质。

4.5 展览浏览页(ExhibitViewingPageTemplate1)

沉浸式看展体验:

  • 展厅信息卡:外框自上而下渐变并圆角 24,策展人姓名仅加粗(无蓝色下划线),去除花哨样式保持阅读专注。
  • 标题滚动动画:超长标题从右往左单向循环滚动,吸引注意力又不干扰整体布局。
  • 展厅分布:去掉伸缩头,仅保留更小缩略图横向列表,选中高亮且隐藏「x号厅」字样,减少信息噪音。
  • 主视觉区:图片/视频轮播,视频自动播放,图片支持手动滑动。
  • 画框与材质:每个展位作品根据 ExhibitBoothFrameData 渲染真实画框效果,含材质贴图、装饰垫颜色、厚度阴影。
  • 音乐播放:展厅支持背景音乐,右上角音乐按钮实底设计提高清晰度;进入作品详情或放大预览时自动暂停,返回后恢复,状态严格同步避免失步。
  • 弹幕:支持弹幕开关;关闭时以固定高度深色占位(_DanmakuAreaPlaceholder)避免背景发浅突兀。
  • 底部操作栏:左到右渐变背景,点赞/评论/收藏图标使用定制 SVG(icon-exhibit-like.svgicon-exhibit-comment.svg);点赞激活态为红色,收藏激活态为金色,视觉反馈鲜明。
  • 交互防遮挡:自动预览蒙层限制 bottomMediaQuery.padding.bottom + 64,避免拦截底部操作栏的点击。
  • 点赞状态持久化:通过 Riverpod noteEntityProvider 维护展位点赞状态,离开页面再进入不丢失。

4.6 作品详情页(ExhibitArtworkDetailPageTemplate1)

  • 主舞台:在可见区域中垂直居中展示作品,带画框材质渲染;图片未加载时保留最小占位约束,加载完成后释放以自适应宽高比。
  • 视频作品fileType=2 时主舞台缩略预览使用视频播放器(thumbnailLevel=3),支持播放控制;点击「查看超清」进入全屏视频播放器(thumbnailLevel=0 原视频)。
  • 横向滑动切换:左右滑动手势切换同一展厅内其他作品,过滤掉 fileId 为空的展位,仅在有实质内容的展位间跳转。
  • 底部缩略图轮播:展厅内多作品时,底部展示缩略图条,点击或滑动切换。
  • 作品信息弹窗:点击「信息」按钮从底部弹出 AppModalSheet,毛玻璃效果(blurSigma: 10),展示作品名称、作者、推荐描述。
  • 互动操作:点赞、评论、分享;点赞状态实时同步到全局实体仓库。

4.7 展览详情页模板(ExhibitDetailPageTemplate1)

另一种展览浏览形态,更偏向「展览介绍 + 作品概览」:

  • 背景沉浸:全屏背景图 + 60% 黑色遮罩,营造展厅暗环境氛围。
  • 顶部导航栏:白色返回/分享图标,居中展览标题。
  • 主展品轮播PageView.builder 横向滑动,含 10 张展品图,viewportFraction: 0.9 让两侧露出下一张边缘;切换时触发 AnimationController 缩放动效,增强视觉反馈。
  • 缩略图导航:底部横向缩略图列表,选中项放大(45px)并带白色边框(2px),未选中项缩小(35px),点击可跳转对应页面。
  • 底部功能栏:5 个功能按钮(详情、播放、VR、全屏、分享),白色半透明玻璃质感背景(alpha: 0.12,边框 alpha: 0.6),圆角 6px。
  • 详情弹窗:点击「详情」唤起 DraggableScrollableSheet,初始高度 50%,最小 30%,最大 90%;
    • 顶部白色圆角面板,带系统指示条(灰色短横线);
    • 展厅介绍长文本,首行缩进 18px,段落间距 12px,两端对齐;
    • 底部浮起「进入展厅」按钮,橙色渐变(#FF9022)+ 自定义 ClipPath 弧形裁切,两侧箭头图标引导点击。

4.9 多种展览详情模板(Type 1 / 2 / 3)

产品意图:为不同展览类型提供差异化的浏览体验,体现「一展一风格」的策展理念。

Type 1(智慧卡片)

  • 暖米色背景(#FFFFFAF0),横向 PageView 展示 GIF 动图卡片,viewportFraction: 0.85
  • 卡片带缩放动效(非当前页缩小至 0.8),营造立体纵深感;
  • 底部 BackdropFilter 毛玻璃控制栏,含缩略图导航(选中项带橙色边框 #FF8F1F)与功能按钮(标签/播放/VR/详情/全屏/分享);
  • 卡片底部叠加毛玻璃文字层,展示作品名称与作者。

Type 2(雕塑展览)

  • 类似 Type 1 的横向轮播,但每张卡片展示雕塑作品大图 + 标题 + 详细描述;
  • 更适合静态艺术品的深度介绍。

Type 3(画廊模式)

  • 全屏背景图随当前选中作品实时切换,extendBodyBehindAppBar: true 让背景延伸至状态栏;
  • BackdropFilter 高斯模糊背景图作为氛围层;
  • 底部横向缩略图条,选中高亮。

3D 展厅(Exhibition3dPage)

  • 当前为占位页(”3D view will be implemented here”),预留 WebGL/3D 引擎接入能力,未来可支持 360° 虚拟展厅漫游。

4.8 评论页(ExhibitArtworkCommentPageTemplate1)

  • 评论结构:支持一级评论与二级回复,嵌套展示;子评论默认展示前 2 条,超出时「查看全部 x 条评论」可展开。
  • 交互细节
    • 每条评论展示头像、昵称、内容、时间;
    • 支持回复、点赞、删除(仅自己的评论);
    • 评论点赞数实时更新;
    • 时间显示智能格式化(刚刚、x分钟前、x小时前、x天前)。
  • 输入框:底部悬浮输入栏,点击唤起全屏键盘输入 Sheet,带圆角与阴影;支持回复指定用户(hintText: '回复:$replyToNickName')。
  • 空状态:无评论时展示「暂无评论,快来抢沙发吧」。
  • 加载更多:列表滚动到底部 80px 内自动加载更多。

五、笔记内容生态(Note)

5.1 笔记详情页(NoteDetailPage)

两级浏览架构:

  • 一级预览:默认进入 startInPreview=true,展示笔记媒体(图片轮播/视频)+ 基本信息;支持左右滑动浏览图片,双击放大。
  • 二级文本页:点击「查看全文」进入完整文本页;拦截系统返回与顶部返回,先回一级预览,再返回列表,保证「一级→二级→全屏」与「全屏→二级→一级」的导航链完整。
  • 媒体播放
    • 图片使用 CachedNetworkImage,笔记详情轮播图使用缩略图级别 9;
    • 视频使用 VideoPlayerController,支持点击播放/暂停、双击进入全屏、长按拖动进度条(scrub);
    • 首次播放提示「双击可放大播放」,2 秒后自动消失。
  • 背景音乐:笔记详情增加 musicFileId(取 data.videoFileId),详情页自动播放背景音乐并支持右上角按钮播放/暂停;使用 GlobalMusicService 统一音频焦点管理,视频播放时自动暂停背景音乐,视频结束/离开恢复。
  • 评论:底部评论区支持点赞、回复、删除;支持下拉刷新。
  • 生命周期管理:页面 dispose 前暂停所有视频,防止返回上一页后视频继续播放;didPushNext/didPopNext/AppLifecycleState 均做媒体暂停/恢复处理。

5.2 笔记评论系统(NoteCommentSection / NoteCommentController)

架构设计:

  • 使用 FamilyAsyncNotifier<NoteCommentState, int>,以 noteId 为 family key,每个笔记的评论数据独立管理。
  • 评论结构:支持一级评论与二级回复,嵌套展示;replyShowCount 默认 10,UI 层可对超过 3 条的子评论做折叠处理。
  • 分页加载:首次加载 page 1,滚动到底部自动 loadMore()isLoadingMore 状态控制加载指示器,避免重复请求;到达末尾展示「没有更多评论了」。
  • 评论操作
    • 发表评论:支持一级评论(parentId = 0)与回复指定用户(parentId = commentId, replyToUserId = targetUserId);发表成功后自动 refresh() 并同步评论数到全局实体仓库(noteInteractionControllerProvider.updateCommentCount)。
    • 删除评论:递归遍历评论树找到目标评论并移除(removeFromList),同步更新总数;删除后同步全局仓库。
    • 点赞/取消点赞(乐观更新):先本地翻转 isLikedlikeCount 更新 UI,再调用接口;若接口失败则回滚到旧状态,保证用户操作即时反馈且数据一致性。
  • UI 组件NoteCommentItem 展示头像、昵称、内容、时间、点赞数;支持点击回复、点赞、删除(仅自己的评论)。
  • 空状态:「暂无评论,快来抢沙发吧」。

5.3 图片全屏浏览(NoteImageGalleryPage)

  • 支持多图左右滑动查看,全屏黑色背景,沉浸体验。

六、音频展览(Audio Exhibit)

6.1 音频展览发布(AudioExhibitPublishPage)

  • 海报上传区:虚线边框(DashedBorder)设计,点击上传海报图片;上传后支持替换。
  • 音视频上传:支持选择音频或视频文件;视频上传后展示预览,含播放/暂停与进度条。
  • 富文本表单:标题、话题标签、简介、脚本;话题支持 # 提取。
  • 底部发布栏:固定在底部,「发布」按钮带渐变背景。

6.2 音频展览播放(AudioExhibitViewPage)

  • 沉浸式播放器设计
    • 背景:基于封面图的 BackdropFilter 高斯模糊(sigmaX: 30, sigmaY: 30),营造氛围感;
    • 中央:圆角封面卡片(ClipRRect + BoxShadow),悬浮于模糊背景之上;
    • 信息卡:标题、艺术家名、关注按钮;
    • 底部:播放进度条、播放/暂停按钮、评论与歌单按钮。
  • 状态管理:使用 GlobalMusicHandle 管理音频播放状态,支持全局暂停/恢复。

七、个人主页与社交(MyPage / UserProfilePage)

7.1 个人主页结构

  • 顶部 Header:全宽背景图 + BackdropFilter 毛玻璃效果(sigmaX: 30, sigmaY: 30)+ 15% 黑色遮罩;内容区展示头像、昵称、ID、个性签名、关注/粉丝/获赞/收藏数据。
  • 头像预览:点击头像进入全屏 PhotoView,支持双指缩放。
  • Tab 导航(5 个 Tab):笔记、赞过、收藏、展览、生活秀。
  • 瀑布流展示:笔记采用 MasonryGridView 双列瀑布流;展览/生活秀采用固定比例网格(177:181)。
  • 数据懒加载:Tab 切换时首次加载对应数据,滚动到底部 200px 内自动加载更多。
  • 刷新机制:下拉刷新联动当前 Tab 与用户信息;didPopNextAppLifecycleState.resumed 自动刷新用户数据。
  • 展览封面轮播:个人主页展览卡片若有多张图,自动 3 秒轮播,带动画过渡(animateToPage, 600ms, easeInOut)。
  • 社交互动:支持关注/取消关注、私信入口(聊天功能开发中占位)。

7.2 编辑资料(HomeEditProfilePage)

  • 头像上传:点击头像唤起图片选择,支持拍照/相册;右下角悬浮相机图标(白色圆形+阴影)。
  • 昵称与个性签名输入:表单分组设计,标签 + 圆角输入框(borderRadius: 14);签名区最小高度 110,多行输入。
  • 保存按钮:主色背景,带加载态(CircularProgressIndicator)。

7.3 生活秀(Life Show)

产品定位:生活秀是展览的一种轻量表达形态,用户可以将日常创作、活动记录以展览形式呈现,降低正式策展的仪式感门槛。

展示形态:

  • 列表卡片(LifeShowList):纵向列表,每项为圆角 12px 白色卡片,带轻微阴影(0x0A000000)。
    • 顶部封面图(高度 180,铺满圆角裁切),加载失败展示灰色占位图标;
    • 内容区:红色标签(special,如「热门」「精选」)+ 地理位置文案;
    • 展览名称(单行截断,16px 加粗);
    • 推荐描述(两行截断,浅灰色);
    • 底部作者行:作者头像(32px 圆形)+ 昵称 + 点赞/评论/收藏统计图标;
    • 展期信息(startTime - overTime)。
  • 网格卡片(MyLifeShowGrid):个人主页「生活秀」Tab 采用双列网格(childAspectRatio: 177/181)。
    • 封面图铺满,底部渐变遮罩(#00000000 → #99000000)承载标题与位置;
    • 右上角红色角标(special);
    • 右下角悬浮统计胶囊(半透明黑底圆角 10,点赞/评论数)。
  • 数据模型LifeShowItem 包含完整的展览字段(boothListkeywordsauthorlikeCountisLiked 等),支持点赞、收藏、评论的完整交互。

7.4 草稿箱(HomeDraftListPage)

双 Tab 结构:笔记草稿 + 展览草稿

笔记草稿:

  • 使用 _MyNotesGrid 网格展示(与「笔记」Tab 一致),支持点击继续编辑;
  • 点击后进入 HomeCreateNotePage(editNoteId: noteId),加载原有标题、正文、媒体、隐私设置;
  • 编辑返回后自动刷新草稿列表并同步草稿数量。
  • 分页加载:滚动到底部 180px 内自动加载更多。

展览草稿:

  • 懒加载策略:首次切换到「展览」Tab 时才触发数据拉取,减少首屏开销。
  • 展示形式:双列网格(177:181),与已发布展览卡片视觉一致。
  • 继续布展:点击草稿卡片后:
    1. 调用 getExhibitionDetail(id) 拉取完整展览详情;
    2. 通过 fastExhibitDraftProvider.notifier.loadFromExhibition(detail) 将草稿数据回填到快布展状态;
    3. 导航至 FastExhibitPage,用户可在原有基础上继续编辑、增删展厅/展位、更换作品;
    4. 返回草稿箱后自动刷新列表。
  • 空状态:「暂无展览草稿,保存展览草稿后会显示在这里」。

八、设置与系统

8.1 设置页(SettingsPage)

  • 卡片式设置列表:圆角 12px 白色卡片,分组展示「个人信息」「当前版本」「关于一起展」。
  • 注销账号:独立卡片,带警示文案「注销后账号无法恢复,请谨慎操作」。
  • 退出登录:底部全宽按钮,点击后清理 Token、上报 Matomo 埋点、跳转登录页并清空路由栈。

8.2 个人信息页(PersonalInfoPage)

  • 头像展示区:114px 大圆形头像,使用 CachedNetworkImage 加载;右下角悬浮相机图标(28px 圆形白底),提示可编辑;点击跳转「编辑资料」页。
  • 信息列表:白色卡片圆角 12px,展示「昵称」与「简介」两项,每项带标签(浅灰色 14px)+ 值(深灰色 14px)+ 右侧箭头;未设置时显示「未设置」占位文案。
  • 跳转逻辑:点击昵称或简介均跳转 HomeEditProfilePage,用户可在同一页面完成两项编辑。

8.3 关于页(AboutPage)

  • 品牌区:顶部居中展示 App Logo(108px 圆角方形,assets/logo.jpg)+ 应用名称「一起展」(18px 加粗)。
  • 版本信息:圆角 12px 白色卡片,展示「当前版本」与「版本更新」;版本更新项带深蓝色标签(#1C2F5D)展示最新版本号,点击提示「已是最新版本」。
  • 法律合规:独立白色卡片展示「用户协议」「隐私政策」,点击唤起对应协议页。
  • 底部版权:页脚展示「@ 2025-2026 上海艾魅之文化艺术有限公司. All Rights Reserved」+ ICP 备案号「沪ICP备2025146963号-7A」+ 版本号,字体 11px 灰色,体现平台合规性。

8.4 账号注销页(AccountDeletePage)

  • 警示性页面,引导用户了解注销后果,确认后执行账号注销流程。

九、扫码功能(QrScanPage)

  • 使用 mobile_scanner 实现二维码扫描。
  • 权限处理:首次进入检查相机权限,被拒绝时展示「需要摄像头权限」引导页,提供「重试」与「去设置」两个选项。
  • 扫描界面:黑色背景,中央挖空矩形框(PathFillType.evenOdd 实现遮罩),白色边框 + 圆角 16;顶部 AppBar 支持手电筒开关与前后摄像头切换。
  • 结果处理:扫码结果若是 URL 则尝试外部浏览器打开;否则弹窗展示文本内容;处理完成后自动关闭扫描页。

十、设计与交互细节汇总

10.1 视觉一致性

  • 主题系统:所有颜色、文字样式、间距均沉淀在 design_system 包的 AppColorsAppTextStylesAppDimens 中,严禁使用魔法数字。
  • 渐变运用:登录按钮紫蓝渐变、发布按钮深蓝渐变、底部栏渐变、展厅信息卡渐变——渐变方向与色值经过精确计算,强化品牌质感。
  • 圆角体系:不同场景使用不同圆角半径(小至 2px 的 Chip,大至 24px 的展厅卡,999px 的圆形头像),层次分明。

10.2 动效与微交互

  • 点赞动画:双击视频/作品时红心浮起并淡出。
  • 标题滚动:展览页超长标题单向循环滚动,避免截断又不占用额外空间。
  • 状态切换过渡:Tab 指示器、Chip 选中态、按钮态均有过渡动画(AnimatedOpacityAnimatedContainer)。
  • 图片加载占位:统一使用 AppImageLoadingPlaceholder,避免布局跳动。

10.3 性能优化

  • 图片分级加载thumbnailLevel 机制(0=原图/视频,1=大图,2=中图,3=小图,9=笔记轮播缩略图),不同场景按需加载不同尺寸。
  • 视频预加载:视频频道提前预热下一条视频控制器。
  • Riverpod 状态共享:点赞、收藏等交互状态通过全局 noteEntityProvider 共享,避免页面间状态不同步。
  • RepaintBoundary:复杂独立图形区域使用 RepaintBoundary 减少不必要的重绘。

10.4 安全与隐私

  • 协议前置:登录、注册必须明确同意用户协议与隐私政策。
  • 隐私分级:笔记发布支持「部分可见/不让谁看」的细粒度权限控制,联系人选择支持搜索与多选。
  • 实名认证:办展功能需完成实名认证,未认证用户点击办展触发弹窗引导。

10.5 异常与降级

  • 视频加载失败:降级为封面图展示。
  • 网络证书错误:登录/验证码场景对 HandshakeException 给出明确中文提示。
  • 空状态:所有列表均配备空状态占位(AppEmptyPlaceholder),带图标与引导文案。
  • 认证过期:个人主页监听 AuthExpiredException,弹窗引导重新登录,登录成功后自动刷新全部数据。

十二、全局实体状态管理与跨页面同步

问题背景:在传统 Flutter 开发中,跨页面的点赞、收藏、关注状态很容易因各自维护独立状态而失步。用户在一个页面点赞后,回到列表页或进入另一个页面,状态可能回退到旧值。

「一起展」的解决方案:

  • 全局实体仓库:在 shared_models 中定义 noteEntityProviderStateProvider.family<NoteEntity?, int>)与 exhibitionEntityProvider,以笔记/展览 ID 为 family key,任何页面均可通过同一 Provider 读写同一实体的状态。
  • 批量同步机制batchUpsertNoteEntities(ref, entities)batchUpsertExhibitionEntities(ref, entities) 在列表页收到接口返回后,一次性将所有条目注入全局仓库;详情页、交互控制器则从同一仓库读取最新状态。
  • 交互控制器架构
    • noteInteractionControllerProvider:封装点赞/取消点赞逻辑,成功后更新全局 noteEntityProvider 中对应实体的 isLikedlikeCount
    • exhibitionInteractionControllerProvider:同理处理展览的点赞与收藏。
  • 状态持久化效果:用户在看展页点赞某作品 → 退出看展页 → 再次进入同一展览,点赞状态保持红色高亮,不会回退到初始值。
  • 跨页面即时同步:用户在笔记详情页点赞 → 返回首页瀑布流 → 列表中该笔记的点赞数与红心状态已同步更新(因首页 didPopNext 刷新拉取最新数据,同时全局仓库保证了无刷新时的状态一致性)。

十三、音频焦点与全局媒体协调

问题背景:移动设备同时只能有一个音频焦点。当用户在看展页播放背景音乐,又切换到笔记详情页观看视频,或者进入音频展览页播放语音导览时,必须有一套协调机制防止多个音源互相抢占、混乱播放。

「一起展」的协调策略:

  • GlobalMusicService:基于 just_audio 封装的全局音乐服务,所有非视频类音频(展厅背景音乐、音频展览)统一由此服务播放。
  • MediaPlaybackCoordinator:视频播放器(video_player)在初始化/播放前向协调器注册;协调器确保同一时刻只有一个视频在播放;新视频开始播放时,自动暂停其他视频与背景音乐。
  • 音频焦点抢占与恢复
    • 视频播放 → 自动暂停 GlobalMusicService
    • 视频结束/离开视频页 → 若当前页有背景音乐需求,自动恢复播放;
    • 展厅背景音乐开启 → 视频预加载但不自动播放,等待用户手动点击后才抢占焦点。
  • 展厅内音乐状态精细管理
    • 进入作品详情页或放大预览 → _pauseMusicTemporarily() 暂停音乐,同时更新 _currentMusicPlaying = false
    • 返回展厅 → _resumeMusicTemporarily() 恢复音乐,同时更新 _currentMusicPlaying = true
    • 避免因状态缓存过期导致音乐按钮点击无效(_scheduleSyncMusic 中每次校验 _musicHandle.isPlaying,而非依赖过期的本地布尔值)。
  • Android 音频焦点不中断VideoPlayerController 增加 VideoPlayerOptions(mixWithOthers: true),避免 Android 系统强制抢占音频焦点导致背景乐被系统级中断后无法恢复。

十四、展览封面解析策略

封装位置lib/features/exhibit/domain/exhibit_cover_resolver.dart

解析规则(体现对多媒体混合场景的细致处理):

  • 传入 files 列表(含 fileId + fileType)。
  • 首项为视频(fileType==2:仅使用视频封面(thumbnailLevel=2),不再额外拼接图片,避免视频封面与静态图风格不一致造成视觉跳跃。
  • 首项为图片(fileType==1:提取前 2 张图片(自动跳过视频),用于封面轮播;若不足 2 张则有几张展示几张。
  • 海报优先:优先使用 visitFileId 作为海报;若展位首项为图片,则海报 + 首张展位图轮播;若首项为视频,则仅展示海报。
  • 接入点:已统一接入 ExhibitRepository 的展览列表、我的展览列表、模板列表组装逻辑,确保全 App 封面展示规则一致。

十五、一些设计与优化

优化项 问题 解决方案
状态栏自适应 从视频/看展页返回首页后,状态栏图标颜色残留,导致在白底上不可见 首页根节点包裹 AnnotatedRegion<SystemUiOverlayStyle>,视频 Tab 强制 light,其余强制 dark
展厅分组规则 旧逻辑要求 boothNo 必须完整覆盖 1..boothCount,导致合法但不连续的展位无法渲染 放宽为:只要存在合法 boothNo 即渲染;fileId 为空的展位占位显示;仅当所有 booth fileId 为空时整厅不渲染
交互按钮不可点 自动预览蒙层 Positioned.fill + GestureDetector(opaque) 覆盖了底部点赞/评论/收藏栏 限制蒙层 bottom: MediaQuery.padding.bottom + 64,仅覆盖内容区,避开底部操作栏
音乐按钮状态失步 缓存的 _currentMusicPlaying 布尔值与真实音频播放状态不一致,导致点击无效 每次同步时以 _musicHandle.isPlaying 为准;临时暂停/恢复时同步更新本地状态
点赞状态回退 离开展厅再进入,点赞状态回退到初始值 全部迁移到 Riverpod 全局实体仓库,以真实 ID 为键持久化交互状态
视频预加载 滑动切换视频时频繁出现黑屏/卡顿 开始播放约 5 秒后预热下一条视频的 VideoPlayerController,优先保证滑动流畅
动态展位布局 发布页固定 5 个图位,无法适配不同模板的展位数量差异 按模板 boothList.length 动态生成展位,提交时也按实际展位数提交 boothList
封面解析一致性 不同页面展览封面展示规则不一,视频封面与图片混排时视觉混乱 统一封装 exhibit_cover_resolver,按 fileType 优先级提取,全 App 复用同一策略
展厅信息卡设计 策展人姓名蓝色下划线过于花哨,干扰阅读;音乐按钮透明层模糊不清晰 策展人姓名仅加粗无下划线;音乐按钮改为实底设计,提高可识别度
发布成功后路由 发布成功或保存草稿后,用户迷失在布展流程中,不知道回到了哪里 发布成功 → 首页「我的」Tab;保存草稿 → 草稿箱「展览」Tab,明确告知用户内容归属

十六、路由体系(AppRoutes / AppRouter)

架构原则:壳工程 host_app 集中注册所有路由,各 feature_* 包仅依赖 core_router 的抽象,禁止互相 import 其他 feature 的 Page 类。

已注册路由清单:

路由名 页面 关键参数
splash SplashPage
login LoginPage
bindPhone BindPhonePage
agreement AgreementPage type(AgreementType)
home HomePage
homeCreateNote HomeCreateNotePage
homeDrafts HomeDraftListPage initialTabIndex(0=笔记,1=展览)
noteDetail NoteDetailPage noteId, startInPreview, enableBackToPreviewWhenTextMode
audioExhibitPublish AudioExhibitPublishPage
audioExhibitView AudioExhibitViewPage coverUrl, title, artist
fastExhibit FastExhibitPage
exhibitTab ExhibitTabPage
exhibitCoverTemplate1 ExhibitCoverPageTemplate1 hall(ExhibitHall)
exhibitDetailTemplate1 ExhibitDetailPageTemplate1
exhibitDetailType1/2/3 多种展览详情模板
exhibitViewingTemplate1 ExhibitViewingPageTemplate1 title, exhibitionId, initialDetail
exhibitArtworkDetailTemplate1 ExhibitArtworkDetailPageTemplate1 作品相关全量参数(imageUrl, visitFileId, artworkAuthor 等)
exhibitArtworkCommentTemplate1 ExhibitArtworkCommentPageTemplate1 title, commentCount, exhibitionId, boothId
exhibition3d Exhibition3dPage
userProfile UserProfilePage userId
mySettings SettingsPage
myPersonalInfo PersonalInfoPage
myAbout AboutPage
myAccountDelete AccountDeletePage

参数传递规范

  • 所有参数统一通过 Map<String, Object?> 传递;
  • _args() 辅助函数自动转换参数类型,兼容 MapMap<String, Object?>
  • 每个路由构造器负责从 args 中提取字段并赋予默认值,避免空值崩溃。

十七、设计系统组件(Design System)

packages/common/design_system 沉淀了全 App 复用的视觉原子与分子组件,确保跨 feature 体验一致性。

17.1 主题 Token

  • AppColors:全部颜色常量(textPrimary, textSecondary, textTertiary, buttonPrimary, link, inputBorder, surface, backgroundSecondary 等),无魔法数字。
  • AppTextStyles:按字号/字重/行高分级(loginTitleText28, body12, body14, body16, fastExhibitTitle, button16 等),保证全 App 字体层级统一。
  • AppDimens:间距、圆角、高度等维度常量(pageHorizontalPadding, exhibitBannerHeight, exhibitContentTopOverlap, inputRadius 等)。

17.2 通用组件

  • AppEmptyPlaceholder:空状态占位,支持自定义标题、副标题、图标;全 App 列表空状态统一使用。
  • AppSkeletonHallGrid:骨架屏网格,用于展览/笔记列表首次加载时的占位,减少白屏焦虑。
  • AppImageLoadingPlaceholder:图片加载中占位,统一灰色背景 + 加载图标,避免布局跳动。
  • AppModalSheet:底部弹窗封装,支持 blurSigma 毛玻璃效果、圆角、自定义内容区;用于作品信息、筛选、分享等场景。
  • AppTopToast:顶部轻提示,用于操作成功/失败的非阻塞反馈(如「已是最新版本」「加载更多失败」)。

17.3 表单与输入

  • DashedBorder:虚线边框容器,用于海报/封面上传区的「点击上传」视觉引导。
  • AppMediaPicker:统一封装图片/视频/音频选择器,支持拍照、相册、文件选择,供所有 feature 复用。

十八、埋点与分析(Matomo)

集成方式core_analytics 包封装 Matomo 上报,提供 AppAnalyticsService.instance.trackEvent()trackPageView()

埋点覆盖场景:

  • 页面浏览:所有路由跳转均携带 matomoTitle(如「首页-推荐」「办展分类」「笔记详情」),用于漏斗分析。
  • 用户行为:登录/登出、点赞/取消点赞、收藏、评论、分享、发布笔记/展览、保存草稿。
  • 视频播放:视频频道自动上报播放时长、播放错误率、播放完成率。
  • 布展漏斗:进入办展分类 → 点击模板 → 开始布展 → 填写信息 → 发布成功,全流程追踪转化率。
  • 认证过期AuthExpiredException 触发时单独上报,用于监控登录态稳定性。

隐私合规

  • 用户登出时调用 clearVisitorUser(),清除 Matomo 访客标识;
  • 所有事件 category/action/name 均使用英文枚举,便于国际化团队理解。

十九、分享系统(AppShareSheet / AppShareService)

产品定位:分享是内容社区的核心传播链路,「一起展」为笔记、展览、作品、个人主页均提供了统一的分享能力。

UI 组件(AppShareSheet)

  • 底部弹窗showModalBottomSheet 实现,顶部圆角 16px,毛玻璃背景(BackdropFilter, sigmaX: 16, sigmaY: 16),高度固定 254px;
  • 标题栏:左侧「分享」标题(20px 加粗),右侧圆形关闭按钮(28px,浅灰背景 + 深色关闭图标);
  • 分享渠道:横向滑动排列的渠道图标:
    • 微信:54px SVG 图标 + 13px 标签;
    • 朋友圈:同上;
    • 复制链接:54px 圆形灰底图标 + 链接符号;
    • 未启用的渠道(私信/微博/QQ 空间)置灰处理(opacity: 0.4),点击提示「该分享渠道暂未开放」。
  • 渠道状态管理:通过 enabledChannels 参数灵活控制哪些渠道可用,不同场景可差异化配置。

分享服务(AppShareService)

  • 注册机制registerGlobalHandler() 在 App 启动时注入全局分享处理器,实现 feature 包与具体分享 SDK 的解耦;
  • 微信分享:基于 wechat_kit 实现,支持分享到微信会话(kSession)和朋友圈(kTimeline);
  • 链接复制:调用 Clipboard.setData 将分享链接复制到剪贴板,成功后提示「链接已复制」;

分享链接构建(ShareUrlBuilder)

  • 统一域名https://app.amaz-cn.com/app/share/,便于 Deeplink 解析与追踪;
  • 支持类型:笔记详情、展览观看、作品详情、用户主页;
  • 参数结构?type=xxx&noteId=xxx&exhibitionId=xxx&boothId=xxx&userId=xxx
  • 解析能力ShareUrlBuilder.parse(Uri) 可反向解析分享链接,用于 Deeplink 跳转(如从微信 H5 唤醒 App 并直达内容页)。

使用场景

  • 笔记详情页底部栏 → 分享笔记;
  • 展览浏览页 → 分享展览;
  • 作品详情页 → 分享单幅作品;
  • 个人主页 → 分享用户主页。

注:本文档基于代码遍历整理,后续可根据实际产品迭代持续补充完善。

分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理